/**
 * 
 */
package info.niwota.web.bt;

import info.niwota.web.Debug;

import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Properties;

import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.UUID;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;

import jvs.peer.io.ChannelManager;
import jvs.peer.io.DataPacketService;
import jvs.peer.io.Message;
import jvs.peer.io.PooledInputStream;
import jvs.peer.io.PooledOutputStream;

import org.apache.commons.pool.KeyedPoolableObjectFactory;
import org.apache.commons.pool.impl.GenericKeyedObjectPool;

/**
 * @author qiangli
 * 
 */
public class BlueCove extends DataPacketService {

	class PooledSocket implements StreamConnection {

		private StreamConnection conn = null;

		private PooledInputStream inputStream;

		private PooledOutputStream outputStream;

		private boolean connected = false;

		private ReceiverThread receiver;

		public PooledSocket() {
			this.conn = null;
			this.inputStream = null;
			this.outputStream = null;
			this.connected = false;
		}
		
		public void init(StreamConnection conn) throws Exception {
			if (this.conn != null) {
				destroy();
			}
			this.conn = conn;
		
			//
			PooledInputStream in = this.openInputStream();
			startReceiver(in);
			
			OutputStream out = this.openOutputStream();
			out.flush();
			//
			this.connected = true;
		}

		private void startReceiver(PooledInputStream in) {
			if (receiver != null) {
				receiver.interrupt();
			}
			receiver = null;
			//
			receiver = new ReceiverThread(in);
			receiver.setDaemon(true);
			receiver.start();
		}
		
		public void close() throws IOException {
			flush();
		}

		public DataInputStream openDataInputStream() throws IOException {
			throw new IOException("Not supported");
		}

		public DataOutputStream openDataOutputStream() throws IOException {
			throw new IOException("Not supported");
		}

		public PooledInputStream openInputStream() throws IOException {
			if (inputStream == null) {
				try {
					inputStream = new PooledInputStream(conn.openInputStream());
				} catch (IOException e) {
					destroy();
					
					throw e;
				}
			}
			return inputStream;
		}

		public PooledOutputStream openOutputStream() throws IOException {
			if (outputStream == null) {
				try {
					outputStream = new PooledOutputStream(conn.openOutputStream());
				} catch (IOException e) {
					destroy();
					
					throw e;
				}
			}
			return outputStream;
		}

		public void destroy() {
			connected = false;

			close(inputStream);
			close(outputStream);
			close(conn);
			
			this.conn = null;
			this.inputStream = null;
			this.outputStream = null;
		}

		private void close(StreamConnection s) {
			if (s == null) {
				return;
			}
			try {
				s.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		private void close(Closeable s) {
			if (s == null) {
				return;
			}
			try {
				s.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		public boolean isClosed() {
			return !connected;
		}

		public void flush() throws IOException {
			if (outputStream != null) {
				outputStream.flush();
			}
		}
	}

	private static final int BUF_SIZE = 8192;

	private static int counter = 0;

	private class SocketPoolableObjectFactory implements
			KeyedPoolableObjectFactory {

		@Override
		public Object makeObject(Object key) throws Exception {
			if (Debug.DEBUG) {
				System.out.println("makeObject ####### [" + counter++ + "] " + key);
			}
			return new PooledSocket();
		}

		@Override
		public void destroyObject(Object key, Object obj) throws Exception {
			if (Debug.DEBUG) {
				System.out.println("destroyObject [" + counter + "] " + key);
			}
			PooledSocket so = (PooledSocket) obj;
			try {
				so.destroy();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		@Override
		public boolean validateObject(Object key, Object obj) {
			if (Debug.DEBUG) {
				System.out.println("validateObject [" + counter + "] " + key);
			}
			return true;
		}

		@Override
		public void activateObject(Object key, Object obj) throws Exception {
			if (Debug.DEBUG) {
				System.out.println("activateObject [" + counter + "] " + key);
			}
			
			PooledSocket so = (PooledSocket) obj;
			
			if (so.connected) {
				return;
			}
			
			//
			StreamConnection conn = createConnection((String) key);
			so.init(conn);
		}

		@Override
		public void passivateObject(Object key, Object obj) throws Exception {
			if (Debug.DEBUG) {
				System.out.println("passivateObject [" + counter + "] " + key);
			}
			PooledSocket so = (PooledSocket) obj;
			so.flush();
		}
	}

	private SocketPoolableObjectFactory factory = new SocketPoolableObjectFactory();

	private GenericKeyedObjectPool pool = new GenericKeyedObjectPool(factory, 1);

	private ListenerThread listener;
	
	private final String local;

	private final boolean colonAddress;

	public BlueCove() throws BluetoothStateException {
		local = LocalDevice.getLocalDevice().getBluetoothAddress();
		colonAddress = local.indexOf(":") > 0;
	}

	private static String convertAddress(String address, boolean colonAddress) {
		// Android wants colon, Bluecove accepts both
		if (address.indexOf(":") > 0 && colonAddress) {
			return address;
		}
		if (address.indexOf(":") < 0 && colonAddress == false) {
			return address;
		}
		// convert
		final int len = address.length();
		StringBuilder sb = new StringBuilder();
		if (colonAddress) {
			for (int i = 0; i < len; i += 2) {
				sb.append(address.charAt(i));
				sb.append(address.charAt(i + 1));
				if (i + 2 < len) {
					sb.append(":");
				}
			}
		} else {
			for (int i = 0; i < len; i++) {
				char ci = address.charAt(i);
				if (ci == ':') {
					continue;
				}
				sb.append(ci);
			}
		}
		return sb.toString();
	}

	@Override
	public void startup() throws Exception {
		if (listener != null) {
			try {
				shutdown();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		startListener();
	}

	private void startListener() {
		pool.clear();

		listener = new ListenerThread();
		listener.start();
	}

	@Override
	public void shutdown() throws Exception {
		try {
			listener.interrupt();
			listener = null;
		} catch (Exception e) {
			e.printStackTrace();
		}

		pool.clear();

	}

	@Override
	public Message receive() throws IOException {
		Message msg = super.receive();
		
		if (Debug.TRACE) {
			System.out.println(this + " revd<<< " + msg);
		}
		
		return msg;
	}

	@Override
	public void send(Message msg) throws Exception {
		msg.getHeader().setFrom(local);

		final String peer = msg.getHeader().getPeer();
		final String key = convertAddress(peer, colonAddress);

		if (Debug.DEBUG) {
			System.out.println("send peer " + peer + " key: " + key);
		}

		PooledSocket sc = null;
		PooledOutputStream out = null;
		try {
			sc = (PooledSocket) pool.borrowObject(key);

			out = sc.openOutputStream();

			send(msg, out);
			
			if (Debug.TRACE) {
				System.out.println(this + " sent>>> " + msg);
			}
			//
			out.flush();

		} catch (Exception e) {
			if (sc != null) {
				pool.invalidateObject(key, sc);
			}
			
			pool.clear(key);
			
			if (out != null) {
				out.destroy();
			}
			throw e;
		}  finally {
			if (sc != null) {
				pool.returnObject(key, sc);
			}
		}
	}

	protected void receive(StreamConnection conn) throws Exception {
		final String peer = RemoteDevice.getRemoteDevice(conn)
				.getBluetoothAddress();
		final String key = convertAddress(peer, colonAddress);
		
		if (Debug.DEBUG) {
			System.out.println("receive peer: " + peer + " key: " + key);
		}
		
		PooledSocket sc = null;

		try {
			sc = (PooledSocket) pool.borrowObject(key);
			
			//replace with incoming conn
			sc.init(conn);

		} catch (Exception e) {
			if (sc != null) {
				pool.invalidateObject(key, sc);
			}
			
			pool.clear(key);
			
			throw e;
		} finally {
			if (sc != null) {
				pool.returnObject(key, sc);
			}
		}
	}

	public static RemoteDevice getDevice(String address) throws Exception {
		return RemoteDeviceDiscovery.retrieve(address);
	}

	public static String findService(RemoteDevice device) throws Exception {
		UUID uuid = BluetoothConstants.HOTWEB_UUID;
		String url = ServiceSearch.findService(device, uuid);
		return url;
	}
	
	protected class ReceiverThread extends Thread {
		private PooledInputStream in;
		public ReceiverThread(PooledInputStream in) {
			this.in = in;
		}

		public void run() {
			setName("Receive");
			
			try {
				receive(in);
			} catch (Exception e) {
				e.printStackTrace();
				
				in.destroy();
			}
		}
	}

	protected class ListenerThread extends Thread {

		public void run() {
			
			final String url = BluetoothConstants.HOTWEB_URL;

			if (Debug.INFO) {
				System.out.println("Service: " + url);
			}
		
			StreamConnectionNotifier notifier = null;

			while (true) {
				try {
					if (Debug.INFO) {
						System.out.println("Opening bluetooth... " + url);
					}

					LocalDevice.getLocalDevice().setDiscoverable(
							DiscoveryAgent.GIAC);

					notifier = (StreamConnectionNotifier) Connector.open(url);
				} catch (BluetoothStateException e) {
					if (Debug.DEBUG) {
						System.err.println("" + e.getLocalizedMessage());

						e.printStackTrace();
					}
					
					sleep();

					continue;
				} catch (Exception e) {
					e.printStackTrace();

					sleep();
					//
					continue;
				}
				
				StreamConnection conn = null;
				
				while (true) {
					try {

						if (Debug.DEBUG) {
							System.out.println("accepting... ");
						}

						conn = notifier.acceptAndOpen();
						
						if (Debug.DEBUG) {
							System.out.println("accepted " + conn);
						}
						
						handle(conn);
						
						if (Debug.DEBUG) {
							System.out.println("handled " + conn);
						}
						
					} catch (Exception e) {
						e.printStackTrace();

						break;
					}
				}

				close(conn);
				close(notifier);
			}
		}

		private void handle(StreamConnection conn) {
			try {
				receive(conn);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		private void sleep() {
			try {
				if (Debug.DEBUG) {
					System.out.println("waiting... ");
				}
				
				Thread.sleep(60000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		private void close(StreamConnectionNotifier s) {
			if (s == null) {
				return;
			}
			try {
				s.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		private void close(StreamConnection conn) {
			if (conn == null) {
				return;
			}
			try {
				conn.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	public static void init() throws Exception {
		final String network = "btspp";
		Properties attrs = System.getProperties();
		attrs.put(ChannelManager.CHANNEL_DRIVER_CLASS,
				"info.niwota.web.bt.BlueCove");
		ChannelManager.getInstance(network).init(attrs);
		ChannelManager.getInstance(network).startup();
	}

	private StreamConnection createConnection(String peer) throws Exception {
		final RemoteDevice device = getDevice(peer);

		if (device == null) {
			throw new IOException("Device not found: " + peer);
		}

		if (Debug.DEBUG) {
			System.out.println("createConnection device: " + device);
		}

		final String url = findService(device);

		if (Debug.DEBUG) {
			System.out.println("createConnection: " + url);
		}
		
		try {
			StreamConnection conn = (StreamConnection) Connector.open(url);
			return conn;
		} catch (Exception e) {
			e.printStackTrace();
			
			throw e;
		}
	}

	@Override
	public int getBufferSize() {
		return BUF_SIZE;
	}
	
}
